常见锁策略

您所在的位置:网站首页 偏向锁 cas 常见锁策略

常见锁策略

2023-04-14 15:03| 来源: 网络整理| 查看: 265

目录

1.常见锁策略

1.1乐观锁vs悲观锁

1.2轻量级锁vs重量级锁

1.3自旋锁vs挂起等待锁

自旋锁

挂起等待锁

1.4互斥锁vs读写锁

1.5公平锁vs非公平锁

公平锁

非公平锁

1.6可重入锁vs不可重入锁

1.7使用锁策略描述synchronized

2.CAS(Compare And Swap)

2.1CAS应用场景

实现原子类

实现自旋锁

2.2CAS的ABA问题

3.synchronized原理

3.1锁升级/锁膨胀

无锁

偏向锁

轻量级锁

重量级锁

3.2锁消除

3.3锁粗化

1.常见锁策略

锁策略不仅仅局限于java,任何与"锁"相关的话题(操作系统,数据库...),都会涉及到锁策略,这些策略是给锁的实现者用来参考的

1.1乐观锁vs悲观锁

这个不是两把具体的锁.而是两类锁,是在锁冲突的概率上进行区分的

乐观锁指的是预测锁竞争不是很激烈(做的工作相对少一些),悲观锁预测锁竞争会很激烈(这里做的工作会多一些).

1.2轻量级锁vs重量级锁

是从锁开销的角度区分的

轻量级锁加锁解锁开销比较小,效率更高.重量级锁加锁解锁开销比较大,效率更低.

多数情况下,乐观锁也是一个轻量级锁,悲观锁也是一个重量级锁

1.3自旋锁vs挂起等待锁

自旋锁是典型的轻量级锁

挂起等待锁是典型的重量级锁]

自旋锁

自旋锁伪代码:

while (抢锁(lock) ==失败) {} 复制代码

自旋锁如果获取锁失败,立即再尝试获取锁,无限循环..一旦锁被其他线程释放,就能第一时间获取到锁

自旋锁的优点: 没有放弃cpu,不涉及线程阻塞和调度,一旦锁被释放,就饿能第一时间获取到锁 缺点: 如果锁被其它线程持有的时间较长,那么就会持续的消耗cpu资源(挂起等待是不需要消耗资源的)

挂起等待锁

挂起等待锁:如果一个锁被另外的线程持有,挂起等待锁会一直等待,不会主动去获取锁

这种做法不会消耗大量cpu资源,就可以做别的工作了.

1.4互斥锁vs读写锁

互斥锁

提供加锁和解锁操作,就像我们使用过的synchronized这样的锁.如果一个线程加锁了,另一个线程也尝试获取锁,就会阻塞等待

读写锁

提供了三种操作

1.针对读加锁 2.针对写加锁 多线程针对同一个变量并发读是没有线程安全问题的.也不需要加锁. 读锁和读锁之间没有互斥 写锁和写锁之间是互斥的 写锁和读锁之间存在互斥 假设一组线程并发读同一个变量,这时线程之间是没有锁竞争的,也没有线程安全问题!假设一组线程有读又有写,才会产生锁竞争..实际开发中,读操作非常高频

3.解锁

1.5公平锁vs非公平锁 公平锁

把公平锁定义为"先来后到"

B比C先来获取锁然后阻塞等待的,当A释放锁之后,B就能先于C获取到锁

非公平锁

不遵守"先来后到"

不管BC谁先来的,当A释放锁之后,BC都有可能获取到锁,synchronized就是非公平锁!

操作系统内部的线程调度就是随机的,如果不做额外的限制,锁就是非公平锁,如果要实现公平锁,就需要额外的数据结构来保存先后顺序 公平锁和非公平锁没有优劣,要看适用的场景

1.6可重入锁vs不可重入锁

不可重入锁:一个线程针对同一把锁,连续加锁两次,出现死锁

可重入锁:一个线程针对同一把锁,连续加锁多次都不会出现死锁

1.7使用锁策略描述synchronized

上述种锁策略,就像是锁的形容词.任何一个锁,都能用上述锁策略来描述,形容,我们看synchronized是怎样的

1.synchronized既是一个悲观锁,又是个乐观锁 synchronized默认是乐观锁,但是如果发现锁竞争比较激烈,就会变成悲观锁!! 2.synchronized既是轻量级锁,又是一个重量级锁 synchronized默认是轻量级锁,当锁冲突剧烈后,就变成重量级锁! 3.synchronized这里的轻量级锁是基于自旋锁的方式实现的 synchronized这里的重量级锁是基于挂起等待锁的方式实现的 4.synchronized不是读写锁 5.synchronized是非公平锁 6.synchronized是可重入锁

2.CAS(Compare And Swap)

一个CAS涉及到以下操作:

我们假设内存中的原数据V,旧的预期值A,需要修改的新值B 1.比较A与V是否相等 2.如果相等,将B写入V 3.返回操作是否成功

编辑

上述交换过程中,大多数不关心B后续的情况了,更关心的是V这个变量的情况.近似可以理解成赋值了

如果AV不同,则没有其他操作

我们看一下CAS的伪代码:

boolean CAS(V,A,B){ if(A == V){ V = B; return true; } return false; } 复制代码

但是CAS的过程并非是通过代码实现的!!而是通过一条CPU指令完成的!CAS操作是原子的,因此它是线程安全的.那么解决线程安全问题除了加锁,就又有个新的思路了. CAS是CPU提供的一个特殊指令,通过这个指令,就可以一定程度的处理线程安全问题!

2.1CAS应用场景 实现原子类

Java标准库中提供的有原子类,之前我们学习线程安全时,写过一个问题,两个线程对同一个变量进行自增操作后,这个变量没有达到预期的结果,我们是通过加锁解决线程安全问题的.这里我们直接使用原子类,就不会出现线程安全问题

AtomicInteger count = new AtomicInteger(); 复制代码

AtomicInteger是原子类,基于CAS实现了自增,自减等操作,此时进行自增等操作不需要加锁,也线程安全的

public class Test { public static void main(String[] args) throws InterruptedException { //使用原子类解决线程安全问题 AtomicInteger count = new AtomicInteger(); Thread t1 = new Thread(()->{ for (int i = 0; i < 50000; i++) { count.getAndIncrement(); } }); Thread t2 = new Thread(()->{ for (int i = 0; i < 50000; i++) { count.getAndIncrement(); } }); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(count); } } 复制代码

结果:

编辑

我们看一下伪代码实现的原子类

class AtomicInteger { private int value; public int getAndIncrement() { int oldValue = value; while ( CAS(value, oldValue, oldValue+1) != true) { oldValue = value; } return oldValue; } } 复制代码

这里的oldValue可以理解为是寄存器中的值,相当于先把内存中的值读到寄存器里

正常情况下,oldValue应该是和value的值是相同的,然后这里发生CAS,把old Value+1写到value中

但是也可能会有:执行完读取value到寄存器中后,线程切换了,另外一个线程也修改了内存中value的值,此时这个线程如果继续执行进行CAS判定,就会认为value和oldValue不相等了

value和oldValue不相等,然后重新读取oldValue

我们画图解释一下这个过程:

编辑

按照这个时间执行两个线程

t1,t2都进行加载

编辑

然后t2开始CAS

比较oldValue和value的值,发现相等,oldValue+1赋给value

编辑

t2线程执行完毕,切换回t1线程,t1线程开始CAS,发现oldValue和value的值不相等,返回false,不进行任何交换...然后进入循环,循环内部重新读取value的值到oldValue 中,此时再次比较,发现相等了,进行CAS操作,并返回true,循环结束

编辑

原子类这里的实现,每次修改之前都会再确认一下这个值是否符合要求

CAS是属于特殊方法,特定场景能使用,加锁操作是通用方式,各种场景都能使用,打击面很广!

实现自旋锁

我们看一下自旋锁的伪代码

public class SpinLock { private Thread owner = null; public void lock(){ // 通过 CAS 看当前锁是否被某个线程持有. // 如果这个锁已经被别的线程持有, 那么就自旋等待. // 如果这个锁没有被别的线程持有, 那么就把 owner 设为当前尝试加锁的线程. while(!CAS(this.owner, null, Thread.currentThread())){ } } public void unlock (){ this.owner = null; } } 复制代码

Thread owner是记录当前锁是谁加的 this.owner是检测当前的owner是否是null,如果是null的,就进行交换,也就是把当前的线程的引用赋值给owner.如果赋值成功,此时循环结束,加锁完成! 如果当前锁已经被别的线程占用了,那么owner就不是null的,那么CAS就不会产生赋值,同时返回false,循环继续执行,进行下次判断,这就完成了自旋过程!!

在Java中,并不是直接提供了一个方法CAS.此处伪代码是便于理解

2.2CAS的ABA问题

CAS在运行中的核心是检查oldValue和value是否一致,如果一致,就认为value中途没有被修改过.所以进行下一步操作是没问题的

但是还有可能是中途被修改过,然后又还原回来了.把value值设为A,CAS判定value为A,此时value确实可能始终是A,也有可能本来是A,然后被修改为B,最后又还原成了A!这就是ABA问题

ABA情况大部分是不会对代码/逻辑产生太大影响的,当然也有极端情况,我们看下面这个情景:

如果ATM取钱使用的是CAS来扣款,假设A的账户余额1000,要取500.当按下取款按键时,机器卡顿了,A没忍住多按了几下,此时就会产生bug,可能出现重复扣款的现象

正常情况下,机器卡顿多按两次,t1线程的CAS发现余额是1000,然后就交换成500.扣款成功,然后t2线程加载时余额也是1000,CAS发现余额不是1000,就不扣款.正确的逻辑

编辑

下面这种情况,当t2执行CAS的时候,正好有人给A转入了500.那么余额就变成1000了, 执行CAS操作,又扣了500,出现了bug!!

编辑

当然这种情况出现的概率是很低的,但是还是可能出现,针对这种情况,采取的解决方案就是加入一个版本号,初始版本号是1,每次修改版本号都加1,然后进行CAS的时候,不是以金额多少为准了,是以版本号为准,此时如果版本号没变,就一定没有发生改变

3.synchronized原理

两个线程针对同一个变量加锁,就会阻塞等待.除了上述基本原理,synchronized还有一些内部的优化机制,存在的目的就是为了让锁更高效,好用.

3.1锁升级/锁膨胀

当执行到加锁的代码块儿时,加锁过程就可能经历下面几个升级阶段

无锁

无锁状态,还没开始加锁

偏向锁

进行加锁的时候,首先会进入偏向锁状态

偏向锁,并不是真正的加锁,而只是先占个位置,如果有需要就加锁,没需要就不加锁了

相当于"懒汉模式"提到的懒加载一样,非必要,不加锁

synchronized加锁的时候,并不是真正的加锁,而是先进入偏向锁状态,就相当于做一个标记,如果一直没有别的线程来获取这个锁,那么就不会升级,仅仅只做个标记,因为这个变量本来就只有这个线程要使用,过程也没有出现锁竞争,执行完synchronized{}代码块后,再取消掉标记(偏向锁)即可 但是如果出现了锁竞争,再另一个线程加锁之前,偏向锁会迅速升级为真正的加锁状态!!另一个线程阻塞等待...

轻量级锁

当synchronized发生锁竞争的时候,就会从偏向锁升级为轻量级锁(自旋锁)

此时,synchronized是通过自旋的方式来进行加锁的(就和刚刚伪代码一样的逻辑)

但是,如果很快就释放锁了,自旋是值得的,可以立即获取被释放的锁,反之,迟迟不被释放,那么久迟迟拿不到锁,自旋就不划算了..这时候就需要再次升级了!

重量级锁

一直自旋但是又拿不到锁,synchronized也不会无止境的自旋,此时升级为重量级锁(挂起等待锁)

重量级锁(挂起等待锁)则是基于操作系统原生的API来进行加锁了

linux原生提供了mutex一组API,操作系统北河提供的加锁功能,这个锁是会影响到线程的调度的

此时,如果线程进行了重量级锁的加锁,并且发生了锁竞争,此时线程就会被放入阻塞队列中,暂时不参加CPU的调度了,直到锁被释放了,这个线程才有机会被调度到并有机会获取到锁

锁升级了就不能降级了

3.2锁消除

这是编译器的智能判定,看当前代码是否真的需要加锁,如果这个场景不用加锁,就会自动把加的锁销毁

就像StringBuffer中的关键的方法都是带有synchronized修饰的,就不需要程序员再加锁,加了编译器也会自动销毁!

3.3锁粗化

锁的粒度:synchronized包含的代码越多,粒度就越粗.包含的代码越少,粒度就越细.

通常情况下,粒度细一点比较好,加锁的代码是不能并发执行的,锁的粒度越细,能并发的代码就越多,粒度越粗,能并发的越少.

有些情况,粒度粗反而更好

编辑

这种情况下,两次加锁解锁之间的间隙非常小,反反复复加锁解锁效率低开销大,可以直接加一个大锁,将间隙也包括,效率反而高些,毕竟间隙很小,这块儿代码能不能并发执行影响不大!



【本文地址】


今日新闻


推荐新闻


    CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3